package csm.common.xml;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.Hashtable;
import java.util.List;
import java.util.StringTokenizer;

import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.Namespace;
import org.jdom.input.SAXBuilder;
import org.jdom.xpath.XPath;

import csm.common.SMException;
import csm.common.StringUtil;

/**
 * Jdom XML Document Wrapper
 *
 * @author seungmun
 * @since 2006.1.18.
 */
public class XmlDocument
{
    private Document                     doc        = null;
    private boolean                      hasDefaultNamespace;
    private Hashtable<String, Namespace> namespaces = new Hashtable<String, Namespace>();

    /**
     * Jdom xml Document 객체를 가져옵니다.
     *
     * @return
     */
    public Document getDocument()
    {
        return doc;
    }

    private void checkNamespaces()
    {
        for (Namespace ns : namespaces.values())
        {
            if (ns.getPrefix().equals("") && ns.getURI().length() > 0)
            {
                hasDefaultNamespace = true;
                break;
            }
        }
    }

    private String normalizeXPath(String xpath)
    {
        if (xpath == null)
            return null;
        if (!hasDefaultNamespace)
            return xpath;
        StringBuffer sb = new StringBuffer();
        if (xpath.startsWith("//"))
        {
            xpath = xpath.substring(2);
            sb.append("//");
        }
        StringTokenizer tok = new StringTokenizer(xpath, "/");

        while (tok.hasMoreElements())
        {
            String token = tok.nextToken();
            if (token.indexOf("@") < 0)
            { // attribute가 없는 경우
                if (token.indexOf(":") < 0)
                {
                    sb.append("defns:").append(token).append("/");
                }
                else
                {
                    sb.append(token).append("/");
                }
            }
            else
            { // attribute가 있다면
                int atIndex = token.indexOf("@");
                String tmp = token.substring(0, atIndex);
                if (tmp.length() > 0)
                {
                    if (tmp.indexOf(":") < 0)
                        sb.append("defns:").append(tmp);
                    else
                        sb.append(tmp);
                }

                tmp = token.substring(atIndex + 1);
                if (tmp.indexOf(":") < 0)
                    sb.append("@").append("defns:").append(tmp);
                else
                    sb.append("@").append(tmp);
                sb.append("/");
            }
        }

        String result = sb.toString();

        if (StringUtil.right(result, 1).equals("/"))
            return result.substring(0, result.length() - 1);
        else
            return result;
    }

    @SuppressWarnings("unchecked")
    private void resolveNamespace(Element element)
    {
        List<Namespace> nsList = element.getAdditionalNamespaces();
        Namespace nsDefault = element.getNamespace();

        if (!namespaces.contains(nsDefault))
            namespaces.put(nsDefault.getPrefix(), nsDefault);

        for (int i = 0; i < nsList.size(); i++)
        {
            Namespace ns = (Namespace) nsList.get(i);
            if (!namespaces.contains(ns))
                namespaces.put(ns.getPrefix(), ns);
        }

        List<Element> list = element.getChildren();

        for (int i = 0; i < list.size(); i++)
        {
            Element el = (Element) list.get(i);
            resolveNamespace(el);
        }
    }

    private void appendNamespaceToXPath(XPath xpath)
    {
        for (Namespace ns : namespaces.values())
        {
            if (ns.getPrefix().equals("") && ns.getURI().equals(""))
                continue;

            if (ns.getPrefix().equals(""))
                xpath.addNamespace("defns", ns.getURI());
            else
                xpath.addNamespace(ns);
        }
    }

    /**
     * XPATH에 해당하는 노드를 가져옵니다.
     *
     * @param path
     *            XPATH
     * @return 노드
     * @throws JDOMException
     *             XPATH에 파싱오류가 있는 경우에 발생한다.
     */
    public Object selectSingleNode(String path) throws JDOMException
    {
        XPath xpath = XPath.newInstance(normalizeXPath(path));
        appendNamespaceToXPath(xpath);

        Element el = doc.getRootElement();
        Object obj = xpath.selectSingleNode(el);

        return obj;
    }

    /**
     * XPATH에 해당하는 노드리스트를 가져옵니다.
     *
     * @param path
     *            XPATH
     * @return 노드리스트(Element)
     * @throws JDOMException
     *             XPATH에 파싱오류가 있는 경우에 발생한다.
     */
    @SuppressWarnings("unchecked")
    public List<Element> selectNodes(String path) throws JDOMException
    {
        XPath xpath = XPath.newInstance(normalizeXPath(path));
        appendNamespaceToXPath(xpath);

        Element el = doc.getRootElement();
        List<Element> list = xpath.selectNodes(el);

        return list;
    }

    /**
     * XPATH에 해당하는 노드의 Text값을 가져옵니다.
     *
     * @param path
     *            XPATH
     * @return 노드의 Text값(null인 경우는 노드가 없는 경우에 리턴된다.)
     * @throws JDOMException
     *             XPATH에 파싱오류가 있는 경우에 발생한다.
     */
    public String selectSingleNodeText(String path) throws JDOMException
    {
        Element el = (Element) selectSingleNode(path);

        if (el == null)
            return null;

        return el.getText();
    }

    /**
     * XPATH에 해당하는 노드의 Text값을 Normalize해서 가져옵니다.
     *
     * @param path
     *            XPATH
     * @return 노드의 Text값(null인 경우는 노드가 없는 경우에 리턴된다.)
     * @throws JDOMException
     *             XPATH에 파싱오류가 있는 경우에 발생한다.
     */
    public String selectSingleNodeTextNormalize(String path) throws JDOMException
    {
        Element el = (Element) selectSingleNode(path);

        if (el == null)
            return null;

        return el.getTextNormalize();
    }

    /**
     * 생성자
     *
     * @param document
     *            XML문자열
     * @throws JDOMException
     * @throws IOException
     */
    public XmlDocument(String document) throws JDOMException, IOException
    {
        parse(document);
    }

    /**
     * 생성자
     *
     * @param in
     * @throws JDOMException
     * @throws IOException
     */
    public XmlDocument(Reader in) throws JDOMException, IOException
    {
        parse(in);
    }

    /**
     * 생성자
     *
     * @param file
     * @throws JDOMException
     * @throws IOException
     */
    public XmlDocument(File file) throws JDOMException, IOException
    {
        parse(file);
    }

    /**
     * XML을 분석해서 로드한다.
     *
     * @param s
     * @throws JDOMException
     * @throws IOException
     */
    private void parse(String s) throws JDOMException, IOException
    {
        Reader in = new StringReader(s);
        try
        {
            parse(in);
        }
        catch (Exception e)
        {
            in = new StringReader(s.replaceAll("&\\#x[0-9][0-F]?;", " "));
            parse(in);
        }
    }

    /**
     * XML을 분석해서 로드한다.
     *
     * @param in
     * @throws JDOMException
     * @throws IOException
     *             Reader가 닫혀있을 때 발생한다.
     */
    private void parse(Reader in) throws JDOMException, IOException
    {
        SAXBuilder builder = new SAXBuilder();
        doc = builder.build(in);
        resolveNamespace(doc.getRootElement());
        checkNamespaces();
    }

    /**
     * XML을 분석해서 로드한다.
     *
     * @param file
     * @throws JDOMException
     * @throws IOException
     *             파일이 없는 경우에 발생한다.
     */
    private void parse(File file) throws JDOMException, IOException
    {
        SAXBuilder builder = new SAXBuilder();
        doc = builder.build(file);
        resolveNamespace(doc.getRootElement());
        checkNamespaces();
    }

    /*
     * public void Normalize() { }
     */

    /**
     * 네임스페이스를 조회해서 가져온다.
     *
     * @return
     */
    public Namespace getNamespaces(String prefix)
    {
        return this.namespaces.get(prefix);
    }

    /**
     * 부모 노드에 자식노드를 붙이는 메서드
     *
     * @param parent
     * @param childName
     * @param value
     * @return
     * @throws SMException
     */
    public static Element appendChild(Element parent, String childName, Object value) throws SMException
    {
        if (parent == null)
            throw new SMException("Missing Argument 'parent'");

        Element el = new Element(childName);
        if (value != null)
            el.setText(value.toString());

        parent.addContent(el);
        return el;
    }

    public static void main(String[] args) throws JDOMException, IOException
    {
        File file = new File("d:/xmltest.xml");
        byte[] b = new byte[(int) file.length()];
        FileInputStream fi = new FileInputStream(file);
        fi.read(b);
        fi.close();
        XmlDocument xml = new XmlDocument(new String(b, "UTF-8"));
        Element el = (Element) xml.selectSingleNode("//Result");
        System.out.println(el.getText());
    }
}
